#!/usr/bin/env python3
"""Module to connect to A.R.M to MusicBrainz API"""

import logging
import re
import musicbrainzngs as mb
import werkzeug
from discid import read, Disc

import arm.config.config as cfg
from arm.ripper import utils as u


werkzeug.cached_property = werkzeug.utils.cached_property


def main(disc):
    """
    Depending on the configuration musicbrainz is used
    to identify the music disc. The label of the disc is returned
    or "".
    """
    discid = get_disc_id(disc)
    if cfg.arm_config['GET_AUDIO_TITLE'] == 'musicbrainz':
        return music_brainz(discid, disc)
    return ""


def get_disc_id(disc):
    """
    Calculates the identifier of the disc

    return:
    identification object from discid package
    """
    return read(disc.devpath)


def music_brainz(discid: str, job) -> str:
    """
    Query MusicBrainz for metadata about an audio disc and update the job with release info.

    This function uses the provided disc ID to fetch metadata from MusicBrainz. If successful,
    it processes the release or CD stub information and updates the job's database records
    with relevant details like artist, album title, and release year.

    Parameters
    ----------
    discid : str
        A disc ID string generated by the `discid` package that uniquely identifies an audio CD.
    job
        A job object that contains metadata and handles logging and database updates.

    Returns
    -------
    str
        The label or title of the disc, or an empty string if the disc could not be processed
        or no information was found.

    Notes
    -----
    - Calls `get_disc_info()` to retrieve disc metadata from MusicBrainz.
    - Uses `check_musicbrainz_data()` to parse and store release data.
    - Returns early with an empty string on error.
    """

    disc_info = get_disc_info(job, discid)
    if disc_info == "":
        logging.error("ARM has encountered an error and stopping")
        return ""

    artist_title = check_musicbrainz_data(job, disc_info)
    if artist_title == "":
        logging.error("ARM has encountered an error and stopping")
        return ""

    return artist_title


def get_disc_info(job, discid: str) -> str:
    """
    Retrieve CD release information from MusicBrainz using the disc ID.

    This function contacts the MusicBrainz web service with a given disc ID
    to fetch associated release metadata such as artist credits and recordings.

    Parameters
    ----------
    job
        The job object containing configuration, including the ARM version used in the user agent.
    discid : str
        The disc ID to query in MusicBrainz.

    Returns
    -------
    dict
        A dictionary containing release information if successful.
        Returns an empty string if an error occurs (e.g., network error, invalid disc ID).
    """

    # Tell musicbrainz what your app is, and how to contact you
    # (this step is required, as per the webservice access rules
    # at http://wiki.musicbrainz.org/XML_Web_Service/Rate_Limiting )
    mb.set_useragent(app="arm", version=str(job.arm_version), contact="https://github.com/automatic-ripping-machine")

    # Get CD info from musicbrainz and catch any errors
    try:
        disc_info = mb.get_releases_by_discid(discid, includes=['artist-credits', 'recordings'])
        logging.debug(f"discid: [{discid}]")
        # Debugging, will dump the entire xml/json data from musicbrainz
        # logging.debug(f"disc_info: {disc_info}")

    except mb.WebServiceError as exc:
        logging.error(f"Cant reach MB or cd not found ? - ERROR: {exc}")
        u.database_updater(False, job)
        disc_info = ""

    return disc_info


def check_musicbrainz_data(job, disc_info: dict) -> str:
    """
    Process MusicBrainz metadata for a disc or CD stub and update the job database.

    This function inspects the given `disc_info` dictionary for either full disc metadata
    or CD stub data. It extracts track information, album title, artist, release year,
    and number of tracks. It also attempts to download cover art if available, and
    updates the job's associated metadata in the database.

    Parameters
    ----------
    job
        The job object that contains the current disc processing context, including
        logging and database update functionality.
    disc_info : dict
        A dictionary response from MusicBrainz containing metadata under either a
        'disc' or 'cdstub' key.

    Returns
    -------
    str
        A combined artist and album title string if successful, or empty string
        if no valid data was processed.

    Notes
    -----
    - If 'disc' metadata is present, the function checks for a CD-format release and processes the first one.
    - If only 'cdstub' metadata is available, it uses that limited data instead.
    - Track data is passed to `process_tracks()` to record each entry.
    - Album metadata is recorded using `u.database_updater()`.
    - Attempts to retrieve album artwork if full release data is present.
    - Returns a string containing the "Artist Title" if successful, otherwise False.
    """

    music_data = ""

    # Check if valid disc or cdstub data present in data
    # If not, stop and return an empty string
    if 'disc' not in disc_info and 'cdstub' not in disc_info:
        logging.error("No release information reported by MusicBrainz")
        return music_data

    if 'disc' in disc_info:
        logging.info("Processing as a disc")
        release_list = disc_info['disc'].get('release-list', [])
        logging.debug(f"Number of releases: {len(release_list)}")

        # Check returned data has a release_list (album release info), otherwise return empty
        if len(release_list) > 0:
            # Loop through release data and find first that is a CD
            for i in range(len(release_list)):
                logging.debug(f"Checking release: [{i}] if CD")
                medium_list = release_list[i].get('medium-list', [])
                # Check that medium_list is valid (has data) and that we have returned a CD
                # possible values are "12' Vinyl" or "CD" from testing
                if medium_list and medium_list[0].get('format') == "CD":
                    logging.info(f"Release [{i}] is a CD, tracking on...")
                    logging.debug("-" * 50)
                    process_tracks(job, medium_list[0].get('track-list'))
                    logging.debug("-" * 50)

                    # Update ARM with disc info
                    release = disc_info['disc']['release-list'][i]
                    new_year = check_date(release)
                    title = str(release.get('title', 'no title'))
                    artist = release['artist-credit'][0]['artist']['name']
                    no_of_titles = disc_info['disc']['offset-count']
                    artist_title = artist + " " + title
                    # Set out release id as the CRC_ID
                    args = {
                        'job_id': str(job.job_id),
                        'crc_id': release['id'],
                        'hasnicetitle': True,
                        'year': str(new_year),
                        'year_auto': str(new_year),
                        'title': artist_title,
                        'title_auto': artist_title,
                        'no_of_titles': no_of_titles
                    }
                    logging.info(f"CD args: {args}")
                    u.database_updater(args, job)
                    logging.debug(f"musicbrain works -  New title is {title}  New Year is: {new_year}")

                    # Get album art work
                    logging.info(f"do have artwork?======{release['cover-art-archive']['artwork']}")
                    if get_cd_art(job, disc_info):
                        logging.debug("we got an art image")
                    else:
                        logging.debug("we didnt get art image")
                    music_data = artist_title

    # Run if not a disc, but a cdstub (limited data)
    # No check on release is done here, assuming cdstub is limited to CDs
    elif 'cdstub' in disc_info:
        logging.info("Processing as a cdstub")
        process_tracks(job, disc_info['cdstub']['track-list'], is_stub=True)

        # Update ARM with disc info
        title = str(disc_info['cdstub']['title'])
        artist = disc_info['cdstub']['artist']
        no_of_titles = disc_info['cdstub']['track-count']
        new_year = ''
        artist_title = artist + " " + title
        args = {
            'job_id': str(job.job_id),
            'crc_id': disc_info['cdstub']['id'],
            'hasnicetitle': True,
            'year': new_year,
            'year_auto': new_year,
            'title': artist_title,
            'title_auto': artist_title,
            'no_of_titles': no_of_titles
        }
        logging.info(f"cdstub args: {args}")
        u.database_updater(args, job)
        logging.info("do have artwork?======No (cdstub)")
        logging.debug(f"musicbrain works, but stubbed -  New title is {artist_title}")

        music_data = artist_title

    return music_data


def check_date(release: dict) -> str:
    """
    Extract and normalize the release year from a MusicBrainz release dictionary.

    If a 'date' field exists, trims it to just the year.
    If the full date is in the format 'YYYY-MM-DD', only the year ('YYYY') is returned.
    If no date is available, returns an empty string.

    Parameters
    ----------
    release : dict
        A MusicBrainz release dictionary, possibly containing a 'date' field.

    Returns
    -------
    str
        The release year as a string, or an empty string if no date is present.
    """
    # Clean up the date and the title
    if 'date' in release:
        new_year = str(release['date'])
        new_year = re.sub(r"-\d{2}-\d{2}$", "", new_year)
    else:
        # sometimes there is no date in a release
        new_year = ""
    return new_year


def get_title(discid: str, job) -> str:
    """
    Query MusicBrainz for the album title and artist associated with a disc ID.

    This function uses the MusicBrainz API to retrieve the release title and artist name
    for a given disc. If a valid result is found, it updates the job's database entry
    and returns a sanitized string in the format "Artist-Title". If no valid release is
    found, or an error occurs, it updates the database with a failure and returns
    "not identified".

    Parameters
    ----------
    discid : str
        The disc ID obtained from the `discid` package.
    job
        The job object used for accessing the app version and updating the database.

    Returns
    -------
    str
        The sanitized "Artist-Title" string if the disc is identified,
        or "not identified" if no match is found or an error occurs.

    Notes
    -----
    Avoid using logging in this function prior to the `setup_logging()` call,
    as it may interfere with ARM’s logger initialization.
    """

    # Tell musicbrainz what your app is, and how to contact you
    # (this step is required, as per the webservice access rules
    # at http://wiki.musicbrainz.org/XML_Web_Service/Rate_Limiting )
    mb.set_useragent("arm", version=str(job.arm_version), contact="https://github.com/automatic-ripping-machine")
    try:
        disc_info = mb.get_releases_by_discid(discid, includes=['artist-credits'])
        logging.debug(f"disc_info: {disc_info}")
        logging.debug(f"discid = {discid}")
        if 'disc' in disc_info:
            title = str(disc_info['disc']['release-list'][0]['title'])
            # Start setting our db stuff
            artist = str(disc_info['disc']['release-list'][0]['artist-credit'][0]['artist']['name'])
            crc_id = str(disc_info['disc']['release-list'][0]['id'])
        elif 'cdstub' in disc_info:
            title = str(disc_info['cdstub']['title'])
            artist = str(disc_info['cdstub']['artist'])
            # Different id format, but what can you do?
            crc_id = str(disc_info['cdstub']['id'])
        else:
            u.database_updater(False, job)
            return "not identified"

        clean_title = u.clean_for_filename(artist) + "-" + u.clean_for_filename(title)
        args = {
            'crc_id': crc_id,
            'title': str(artist + " " + title),
            'title_auto': str(artist + " " + title),
            'video_type': "Music"
        }
        u.database_updater(args, job)
        return clean_title
    except (mb.WebServiceError, KeyError):
        u.database_updater(False, job)
        return "not identified"


def get_cd_art(job, disc_info: str) -> bool:
    """
    Retrieve and store CD artwork from MusicBrainz if available.

    This function searches the MusicBrainz release list for the first release that
    contains cover art. It then queries the Cover Art Archive for available images,
    updates the job record with the image URLs, and returns a success flag.

    Parameters
    ----------
    job
        The job object containing the database record to update.
    disc_info : dict
        JSON object returned by the MusicBrainz API containing disc and release metadata.

    Returns
    -------
    bool
        True if artwork was found and saved, False if no artwork was found or an error occurred.

    Notes
    -----
    The function handles common MusicBrainz errors such as:
    - 400: Invalid release ID (not a valid UUID)
    - 404: No release exists with the given MBID
    - 503: Rate limit exceeded or service unavailable
    These errors are logged, and the job record is updated to reflect the failure.
    """
    try:
        # Use the build-in images from coverartarchive if available
        if 'disc' in disc_info:
            release_list = disc_info['disc']['release-list']
            logging.debug(f"release_list: {release_list}")
            first_release_with_artwork = next(
                (release for release in release_list if release.get('cover-art-archive', {}).get('artwork') != "false"),
                None
            )
            logging.debug(f"first_release_with_artwork: {first_release_with_artwork}")

            if first_release_with_artwork is not None:
                # Call function from
                #  https://python-musicbrainzngs.readthedocs.io/en/v0.7/api/#musicbrainzngs.get_image_list
                # 400: Releaseid is not a valid UUID
                # 404: No release exists with an MBID of releaseid
                # 503: Ratelimit exceeded
                artlist = mb.get_image_list(first_release_with_artwork['id'])
                logging.debug(f"artlist: {artlist}")

                for image in artlist["images"]:
                    # We dont care if its verified ?
                    if "image" in image:
                        args = {
                            'poster_url': str(image["image"]),
                            'poster_url_auto': str(image["image"])
                        }
                        u.database_updater(args, job)
                        logging.debug(f"poster_url: {args['poster_url']} poster_url_auto: {args['poster_url_auto']}")
                        return True
        return False
    except mb.WebServiceError as exc:
        u.database_updater(False, job)
        logging.error(f"get_cd_art ERROR: {exc}")
        return False


def process_tracks(job, mb_track_list: dict, is_stub=False):
    """
    Process a list of MusicBrainz tracks and store them in the database.

    Iterates over a list of track dictionaries obtained from MusicBrainz and
    extracts track number, length, and title. These are then stored using
    the `put_track` utility function. Handles both stub and full metadata modes.

    Parameters
    ----------
    job
        The job object that contains metadata and database context.
    mb_track_list : list of dict
        List of track entries from MusicBrainz, either full recordings or stub data.
    is_stub : bool, optional
        If True, process tracks using stub (simplified) structure. Default is False.

    Returns
    -------
    None

    Notes
    -----
    - Tracks with missing or invalid lengths will be logged but still processed.
    - A default title like "Untitled track X" will be used if no title is found in stub mode.
    - Each processed track is stored using `u.put_track()`.
    """
    for (idx, track) in enumerate(mb_track_list):
        track_leng = 0
        try:
            if is_stub:
                track_leng = int(track['length'])
            else:
                track_leng = int(track['recording']['length'])
        except ValueError:
            logging.error("Failed to find track length")
        trackno = track.get('number', idx + 1)
        if is_stub:
            title = track.get('title', f"Untitled track {trackno}")
        else:
            title = track['recording']['title']
        u.put_track(job, trackno, track_leng, "n/a", 0.1, False, "ABCDE", title)


if __name__ == "__main__":
    # this will break our logging if it ever triggers for arm
    disc = Disc("/dev/cdrom")
    myid = get_disc_id(disc)
    logging.debug("DiscID: %s (%s)", str(myid), myid.freedb_id)
    logging.debug("URL: %s", myid.submission_url)
    logging.debug("Tracks: %s", myid.tracks)
    logging.debug("Musicbrain: %s", music_brainz(myid, None))
